// JdsHardware.cs
//
// Implements HomeUX.Components.JdsHardware.
//

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Text.RegularExpressions;
using HomeUX.Utilities;

namespace HomeUX.Components
{

/// <summary>
/// Controls a JDS Technologies IR-XP2 "InfraRed Xpander", which transmits infrared remote control
/// codes that have previously been programmed into the unit.
/// </summary>
///
/// <remarks>
///
/// <para>
/// Only a subset of the full functionality of the hardware can be accessed by this class.
/// </para>
///
/// <para>
/// General usage pattern:
/// </para>
///
/// <list type="number">
/// 	<item><description>Construct an instance of this class.</description></item>
///		<item><description>Hook up events.</description></item>
///		<item><description>Call <s>SimpleSerialHardware{}.Open</s> (inherited from
///			<r>SimpleSerialHardware{}</r>).</description></item>
///		<item><description>To control the hardware, you can use
/// 		<s>SimpleSerialHardware{}.TransmitCommand</s> (inherited from
///			<r>SimpleSerialHardware{}</r>) to send a raw command to the serial port, but in general
///			it's easier to use higher-level control methods like <r>PlayIrSample</r>, which
/// 		constructs and sends a serial command and, when the reply is received, parses the
/// 		hardware's reply and calls a callback method you provide.</description></item>
/// </list>
///
/// <para>
/// Philosophy about exceptions:
/// </para>
///
/// <list type="bullet">
///		<item><description>Programmer errors can lead to exceptions like <n>ArgumentException</n>.
/// 		</description></item>
///		<item><description>Hardware problems (including timeouts) won't lead to exceptions, but
///			rather will cause the following to occur:
///			<list type="bullet">
///				<item><description>The callback method you provided (in the "onComplete" parameter)
///					will be called with <n>null</n> in place of the parsed reply object.  This lets
/// 				you clean up your state.</description></item>
///				<item><description>Events such as <r>WarningsReported</r> will be fired as
///				    appropriate.  This lets you centralize reporting of hardware problems to the user.</description></item>
///			</list>
///			</description></item>
///	</list>
///
/// </remarks>
/// 
[DebuggerDisplay("JdsHardware {HardwareLabel}")]
public class JdsHardware : SimpleSerialHardware<GenericReply>
{
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Constants
    //

    /// <summary>
    /// The message to display when the hardware sends something unexpected via the serial port.
    /// </summary>
    const string UnexpectedOutputMessage = "Unexpected output from hardware: {0}";

    /// <summary>
    /// The number of IR sample addresses per memory bank.
    /// </summary>
    const int AddressesPerBank = 500;

    /// <summary>
    /// Wait this long after playing an IR sample, to give the hardware time to complete
	/// transmission.
    /// </summary>
    const int PlayDelayMilliseconds = 50;

    /// <summary>
    /// The correct response to a Reset command.
    /// </summary>
    const string CorrectResetResponse = "6CC0 A A I R X P 2 E0000FF";

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Public Constants
    //

    /// <summary>
    /// IR sample addresses are in the range 1 to this number, inclusive.  See
    /// <r>PlayIrSample</r>.
    /// </summary>
    public const int MaxAddress = 500;

    /// <summary>
    /// Play counts are in the range 1 to this number, inclusive.  See <r>PlayIrSample</r>.
    /// </summary>
    public const int MaxPlayCount = 255;

    /// <summary>
    /// The number of IR emitter ports.
    /// </summary>
    public const int EmitterPortCount = 6;

    /// <summary>
    /// The command to reset the hardware.
    /// </summary>
    public const string ResetCommand = "6C82C0CC6D";

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private State
    //

    /// <summary>
    /// Indicates if the current command that we sent to the hardware -- and for which we're
    /// currently awaiting a reply -- is <r>ResetCommand</r>.
    /// </summary>
    bool _currentCommandIsReset;

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Public Events
    //

    /// <summary>
    /// Fired when this class has warning(s) to report to the application.  These warnings can be
    /// displayed to the user.
    /// </summary>
    public event EventHandler<JdsHardwareWarningsReportedEventArgs> WarningsReported;

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Public Methods & Related Private Methods
    //

    /// <summary>
    /// Initializes an instance of this class.
    /// </summary>
    ///
    /// <param name="workQueue">The <r>WorkQueue</r> to use for scheduling asynchronous tasks.
    /// 	</param>
    ///
    /// <param name="serialPort">The serial port (real or virtual) to connect to.</param>
    ///
    /// <param name="hardwareLabel">A label to use for referring to this hardware device within
    /// 	warnings, trace output, etc.</param>
    ///
    public JdsHardware(WorkQueue workQueue, VirtualSerialPort serialPort, string hardwareLabel) :
        base(workQueue, serialPort, SimpleSerialHardwareMode.Character, hardwareLabel)
    {
    }

    /// <summary>
    /// Sends <r>ResetCommand</r> to the hardware.
    /// </summary>
    ///
    /// <param name="onComplete">Called on completion of <r>ResetCommand</r>.</param>
    ///
    public void Reset(Action<GenericReply> onComplete)
    {
        _currentCommandIsReset = true;
        TransmitCommand(ResetCommand, delegate(GenericReply innerReply)
        {
            _currentCommandIsReset = false;
            onComplete(innerReply);
        });
    }

    /// <summary>
    /// Plays an IR sample.
    /// </summary>
    ///
    /// <param name="address">The address of the sample to play, between 1 and <r>MaxAddress</r>,
	/// 	inclusive.</param>
	///
    /// <param name="playCount">The number of times to play the sample.</param>
	///
    /// <param name="emitterPorts">Specifies one bit for each emitter port to use, up to a total
	/// 	of <r>EmitterPortCount</r> ports (bits).  The lowest order bit specifies the first
	/// 	emitter port.</param>
	///
    /// <param name="onComplete">Called on completion of the operation.</param>
	///
    public void PlayIrSample(int address, int playCount, int emitterPorts,
		Action<GenericReply> onComplete)
    {
		// check argument validity
        if ((address < 1) || (address > MaxAddress))
        {
            throw new ArgumentOutOfRangeException("address",
                String.Format("An IR sample address must be in the range 1 to {0}, inclusive", MaxAddress));
        }
		else
        if ((playCount < 1) || (playCount > MaxPlayCount))
        {
            throw new ArgumentOutOfRangeException("playCount",
                String.Format("An IR sample play count must be in the range 1 to {0}, inclusive", MaxPlayCount));
        }
		else
        if (emitterPorts >= (1 << EmitterPortCount))
        {
            throw new ArgumentOutOfRangeException("emitterPorts",
                String.Format("Only {0} emitter ports are defined", EmitterPortCount));
        }

		// extract the memory bank number (0-based) from <address> into <memoryBank>
        int memoryBank = address / AddressesPerBank;
        address = address % AddressesPerBank;

		// set <command> to be the JDS command to execute
        string command = String.Format(CultureInfo.InvariantCulture, "6C85{0}{1:X2}{2:X2}{3:X2}CC",
            ((memoryBank == 0) ? "F5" : "F7"), address, playCount, emitterPorts);

		// transmit the command
        TransmitCommand(command, onComplete);

        // the hardware doesn't reply to <command>; instead, we'll end this command after a short
		// interval
        WorkQueue.Add(PlayDelayMilliseconds, delegate()
        {
            EndCommand();
            onComplete(GenericReply.Reply);
        });
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // SimpleSerialHardware Overrides & Related Methods
    //

    /// <summary>
    /// Overrides <r>SimpleSerialHardware{}.ParseReceivedString</r>.
    /// </summary>
    ///
    /// <param name="received">See <r>SimpleSerialHardware{}.ParseReceivedString</r>.</param>
    /// 
    /// <param name="pushBack">See <r>SimpleSerialHardware{}.ParseReceivedString</r>.</param>
    /// 
    /// <param name="currentCommand">See <r>SimpleSerialHardware{}.ParseReceivedString</r>.</param>
    /// 
    /// <param name="onReply">See <r>SimpleSerialHardware{}.ParseReceivedString</r>.</param>
    ///
    protected override void ParseReceivedString(string received, ref string pushBack,
        string currentCommand, Action<GenericReply> onReply)
    {
        // if we come across miscellaneous issue(s) to report (e.g. the driver sends text we don't
		// expect), <warnings> will be set to the issue(s) to report, otherwise it will be null
        LazyStringList warnings = new LazyStringList();

        // if we're waiting for a reply to a command, look for that reply; set <foundReply> to true
		// if the reply is found
        bool foundReply = false;
        if (_currentCommandIsReset)
        {
            int index = received.IndexOf(CorrectResetResponse);
            if (index >= 0)
            {
				// we received the correct reply
                received = received.Substring(0, index) +
					received.Substring(index + CorrectResetResponse.Length);
                foundReply = true;

                // since the hardware does not send notifications on its own, and we processed any
                // command replies above (and removed them from <received>), anything remaining in
                // <received> is unexpected hardware output
                if (received.Length > 0)
                    warnings.Add(String.Format(UnexpectedOutputMessage, received));

                // update driver state to indicate that we're no longer executing a command
                EndCommand();
            }
        }

        // report warnings, if any
		ReportWarnings(warnings);

        // if there was a pending command that completed, call the command's callback function
        if (foundReply)
            onReply(GenericReply.Reply);
        else
            pushBack = received;

    }

    /// <summary>
    /// Fires <r>WarningsReported</r> as needed to inform the application about warnings in a given
	/// list.
    /// </summary>
    ///
    /// <param name="warnings">The warnings, if any.</param>
    ///
	public void ReportWarnings(LazyStringList warnings)
	{
        if ((warnings.Count > 0) && (WarningsReported != null))
        {
            WarningsReported(this, new JdsHardwareWarningsReportedEventArgs()
            {
                Messages = warnings.ListOrNull
            });
        }
	}

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Nested Types
    //

    /// <summary>
	/// Indicates a warning that should be displayed to the user.
    /// </summary>
	///
    class WarningException : Exception
    {
        internal WarningException(string format, params object[] args) :
            base(Util.Format(format, args))
	    {
	    }
    }
}

/// <summary>
/// Arguments of the <r>JdsHardware.WarningsReported</r> event.
/// </summary>
[DebuggerDisplay("Count = {Messages.Count}")]
public class JdsHardwareWarningsReportedEventArgs : EventArgs
{
    /// <summary>
    /// Gets the warnings that were reported.
    /// </summary>
    public ICollection<string> Messages { get; internal set; }
}

}

